package com.nulldev.util.crypto.encryptAPI.rsa;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import javax.crypto.Cipher;

import com.nulldev.util.io.FileUtil;
import com.nulldev.util.java.DataSet;

public class RSA {
	/**
	 * The constant that denotes the algorithm being used.
	 */
	private static final String algorithm = "RSA";	
	
	/**
	 * The private constructor to prevent instantiation of this object.
	 */
	private RSA() {
		
	}
	
	/**
	 * The method that will create both the public and private key used to encrypt and decrypt the data.
	 * 
	 * @return DataSet [Public Key, Private Key]
	 */
	public static DataSet<byte[], byte[]> generateKey() {
		try {
			final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
			keyGen.initialize(2048);

			final KeyPair key = keyGen.generateKeyPair();
			
			return new DataSet<byte[], byte[]>(key.getPublic().getEncoded(), key.getPrivate().getEncoded());

		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	public static boolean isRSASupported() {
		try {
			final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
			return true;
		}catch (Exception e) {
			return false;
		}
	}
	
	/**
	 * The method that will create both the public and private key used to encrypt and decrypt the data.
	 * 
	 * @return DataSet [Public Key, Private Key]
	 */
	public static DataSet<byte[], byte[]> generateKey(final int keyLength) {
		try {
			final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
			keyGen.initialize(keyLength);

			final KeyPair key = keyGen.generateKeyPair();
			
			return new DataSet<byte[], byte[]>(key.getPublic().getEncoded(), key.getPrivate().getEncoded());

		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * The method that will create both the public and private key used to encrypt and decrypt the data.
	 * 
	 * @param publicKeyOutput
	 * 		The path of where the public key will be created.
	 * 
	 * @param privateKeyOutput
	 * 		The path of where the private key will be created.
	 * 
	 * @return {@code true} If this operation was successful, otherwise {@code false}.
	 */
	public static boolean generateKey(final String publicKeyOutput, final String privateKeyOutput) {
		try {
			final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm);
			keyGen.initialize(2048);

			final KeyPair key = keyGen.generateKeyPair();

			try {
				final DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File(publicKeyOutput)));
				dos.write(key.getPublic().getEncoded());
			}catch(Exception e) {
				e.printStackTrace();
			}

			try {
				final DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File(privateKeyOutput)));
				dos.write(key.getPrivate().getEncoded());
			}catch (Exception e) {
				e.printStackTrace();
			}
			
			return true;

		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return false;
	}

	/**
	 * The method that will encrypt an array of bytes.
	 * 
	 * @param key
	 * 		The public key used to encrypt the data.
	 * 
	 * @param data
	 * 		The data in the form of bytes.
	 * 
	 * @return The encrypted bytes, otherwise {@code null} if encryption could not be performed.
	 */
	public static byte[] encrypt(PublicKey key, byte[] data) {		
		try {
			final Cipher cipher = Cipher.getInstance(algorithm);
			cipher.init(Cipher.ENCRYPT_MODE, key);
			return cipher.doFinal(data);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return null;

	}

	/**
	 * The method that will decrypt an array of bytes.
	 * 
	 * @param key
	 * 		The {@link PrivateKey} used to decrypt the data.
	 * 
	 * @param encryptedData
	 * 		The encrypted byte array.
	 * 
	 * @return The decrypted data, otherwise {@code null} if decryption could not be performed.
	 */
	public static byte[] decrypt(PrivateKey key, byte[] encryptedData) {		
		try {
			final Cipher cipher = Cipher.getInstance(algorithm);
			cipher.init(Cipher.DECRYPT_MODE, key);
			return cipher.doFinal(encryptedData);
		} catch (Exception ex) {
		}

		return null;

	}
	
	/**
	 * The method that will re-create a {@link PublicKey} from a serialized key.
	 * 
	 * 
	 * @param publicKeyPath
	 * 		The path of the public key file.
	 * 
	 * @throws Exception
	 * 		If there was an issue reading the file.
	 * 
	 * @return The {@link PublicKey} object.
	 */
	public static PublicKey getPublicKey(final File publicKeyPath) throws Exception {
		return KeyFactory.getInstance(algorithm).generatePublic(new X509EncodedKeySpec(FileUtil.read(publicKeyPath)));
	}
	
	/**
	 * The method that will re-create a {@link PrivateKey} from a serialized key.
	 * 
	 * 
	 * @param privateKeyPath
	 * 		The path of the private key file.
	 * 
	 * @throws Exception
	 * 		If there was an issue reading the file.
	 * 
	 * @return The {@link PrivateKey} object.
	 */
	public static PrivateKey getPrivateKey(final File privateKeyPath) throws Exception {
		return KeyFactory.getInstance(algorithm).generatePrivate(new PKCS8EncodedKeySpec(FileUtil.read(privateKeyPath)));
	}
		
	/**
	 * The method that will re-create a {@link PublicKey} from a public key byte array.
	 * 
	 * @param encryptedPublicKey
	 * 		The byte array of a public key.
	 * 
	 * @throws Exception
	 * 		If there was an issue reading the byte array.
	 * 
	 * @return The {@link PublicKey} object.
	 */
	public static PublicKey getPublicKey(final byte[] encryptedPublicKey) {
		try {
			return KeyFactory.getInstance(algorithm).generatePublic(new X509EncodedKeySpec(encryptedPublicKey));
		}catch (Exception e) {
			throw new UnsupportedOperationException(e);
		}
	}
	
	/**
	 * The method that will re-create a {@link PrivateKey} from a private key byte array.
	 * 
	 * 
	 * @param encryptedPrivateKey
	 * 		The array of bytes of a private key.
	 * 
	 * @throws Exception
	 * 		If there was an issue reading the byte array.
	 * 
	 * @return The {@link PrivateKey} object.
	 */
	public static PrivateKey getPrivateKey(final byte[] encryptedPrivateKey) throws Exception {
		return KeyFactory.getInstance(algorithm).generatePrivate(new PKCS8EncodedKeySpec(encryptedPrivateKey));
	}
}
